package com.yaolonga.dc.exector.r2dbc;

import com.yaolonga.dc.core.*;
import com.yaolonga.dc.core.enums.ConditionType;
import com.yaolonga.dc.core.enums.LogicType;
import com.yaolonga.dc.core.exception.NodeExecuteException;
import com.yaolonga.dc.core.model.SearchModel;
import com.yaolonga.dc.core.spi.NodeExecutor;
import com.yaolonga.dc.core.util.DcApplicationUtil;
import com.yaolonga.dc.core.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import static java.util.Collections.EMPTY_LIST;

/**
 * @Author: yaolong
 * @CreateTime: 2024-10-20  20:10
 * @Description: r2dbc 节点解析器
 * @Version: 1.0
 */
@Slf4j
public class R2dbcNodeExecutor implements NodeExecutor<Query> {

    private R2dbcEntityTemplate r2dbcEntityTemplate;

    @Override
    public NodeParser<Query> nodeParser() {
        if (r2dbcEntityTemplate == null) {
            r2dbcEntityTemplate = DcApplicationUtil.getBean(R2dbcEntityTemplate.class);
        }
        return (nodes, entityClass) -> {
            Criteria empty = Criteria.empty();
            for (GroupNode node : nodes) {
                LogicType logicType = node.getLogicType();
                List<ConditionNode> conditions = node.getConditions();
                switch (logicType) {
                    case or:
                        empty = empty.or(getCriteria(conditions));
                        break;
                    case and:
                        empty = empty.and(getCriteria(conditions));
                }
            }
            return Query.query(empty);
        };
    }

    private static Criteria getCriteria(List<ConditionNode> conditions) {
        Criteria criteria = Criteria.empty();
        for (ConditionNode conditionNode : conditions) {
            String field = conditionNode.getField();
            Object value = conditionNode.getValue();
            LogicType nodeLogicType = conditionNode.getLogicType();
            ConditionType type = conditionNode.getType();
            Criteria.CriteriaStep step = Criteria.where(field);
            Criteria _criteria = Criteria.empty();

            /**
             *  | 'greater_than'
             *   | 'less_than'
             *   | 'greater_than_eq'
             *   | 'less_than_eq'
             *   | 'eq'
             *   | 'not_eq'
             *   | 'in'
             *   | 'not_in'
             *   | 'like'
             *   | 'not_like'
             *   | 'left_like'
             *   | 'left_not_like'
             *   | 'right_not_like'
             *   | 'right_like'
             *   | 'btw'
             *   | 'not_btw'
             *   | 'empty'
             *   | 'not_empty'
             *   | 'is_null'
             *   | 'not_null';
             */

            switch (type) {
                case greater_than:
                    _criteria = step
                            .greaterThan(value);
                    break;
                case less_than:
                    _criteria = step
                            .lessThan(value);
                    break;
                case greater_than_eq:
                    _criteria = step
                            .greaterThanOrEquals(value);
                    break;
                case less_than_eq:
                    _criteria = step
                            .lessThanOrEquals(value);
                    break;
                case eq:
                    _criteria = step
                            .is(value);
                    break;
                case not_eq:
                    _criteria = step
                            .not(value);
                    break;
                case like:
                    if (value instanceof String) {
                        _criteria = step
                                .like(String.format("%%%s%%", value));
                        break;
                    }
                    _criteria = step
                            .like(value);
                    break;
                case left_like:
                    if (value instanceof String) {
                        _criteria = step
                                .like(String.format("%%%s", value));
                        break;
                    }
                    _criteria = step
                            .like(value);
                    break;
                case right_like:
                    if (value instanceof String) {
                        _criteria = step
                                .like(String.format("%s%%", value));
                        break;
                    }
                    _criteria = step
                            .like(value);
                    break;

                case not_like:
                    if (value instanceof String) {
                        _criteria = step
                                .notLike(String.format("%%%s%%", value));
                        break;
                    }
                    _criteria = step
                            .notLike(value);
                    break;
                case left_not_like:
                    if (value instanceof String) {
                        _criteria = step
                                .notLike(String.format("%%%s", value));
                        break;
                    }
                    _criteria = step
                            .notLike(value);
                    break;

                case right_not_like:
                    if (value instanceof String) {
                        _criteria = step
                                .notLike(String.format("%s%%", value));
                        break;
                    }
                    _criteria = step
                            .notLike(value);
                    break;
                case in:
                    _criteria = step
                            .in((List) value);
                    break;
                case not_in:
                    _criteria = step
                            .notIn((List) value);
                    break;
                case btw:
                    if (value instanceof List) {
                        if (((List) value).size() >= 2) {
                            _criteria = step
                                    .between(((List) value).get(0), ((List) value).get(1));
                            break;
                        }
                    }
                    break;
                case not_btw:
                    if (value instanceof List) {
                        if (((List) value).size() >= 2) {
                            _criteria = step
                                    .notBetween(((List) value).get(0), ((List) value).get(1));
                            break;
                        }
                    }
                    break;
                case is_null:
                    _criteria = step
                            .isNull();
                    break;
                case not_null:
                    _criteria = step
                            .isNotNull();
                    break;
            }
            if (nodeLogicType == LogicType.or) {
                criteria = criteria.or(_criteria);
            } else {
                criteria = criteria.and(_criteria);
            }
        }
        return criteria;
    }

    @Override
    public ExecutorType getExecutorType() {
        return ExecutorType.of("r2dbc");
    }


    @Override
    public <T> DCPage<T> executePage(int page, int pageSize, List<GroupNode> astNode, Class<T> bean) {
        try {
            Mono<DCPage<T>> dcPageMono = executePageAsync(page, pageSize, astNode, bean);
            return dcPageMono.block();
        } catch (Exception e) {
            log.error("R2dbcNodeExecutor 执行异常: {}", e.getMessage(), e);
            throw new NodeExecuteException("R2dbcNodeExecutor 执行异常: ", e);
        }
    }

    @Override
    public <T> List<T> execute(List<GroupNode> astNode, Class<T> bean) {
        try {
            return executeAsync(astNode, bean)
                    .buffer()
                    .single((List<T>) EMPTY_LIST)
                    .block();
        } catch (Exception e) {
            log.error("R2dbcNodeExecutor 执行异常: {}", e.getMessage(), e);
            throw new NodeExecuteException("R2dbcNodeExecutor 执行异常: ", e);
        }
    }

    @Override
    public <T> Mono<DCPage<T>> executePageAsync(int page, int pageSize, List<GroupNode> astNode, Class<T> bean) {
        return page(nodeParser().parse(astNode, bean), page, pageSize, bean);
    }


    @Override
    public <T> DCPage<T> executePage(SearchModel searchModel, Class<T> bean) {
        return executePageAsync(searchModel, bean).block();
    }

    @Override
    public <T> Mono<DCPage<T>> executePageAsync(SearchModel searchModel, Class<T> bean) {
        return page(getQuery(searchModel, bean), searchModel.getPage(), searchModel.getPageSize(), bean);
    }

    private <T> Query getQuery(SearchModel searchModel, Class<T> bean) {
        Query parse = nodeParser().parse(searchModel.getAst(), bean);
        if (searchModel.getSorts() != null) {
            List<Sort.Order> orders = searchModel.getSorts()
                    .stream()
                    .filter(sort -> sort.getField() != null)
                    .map(sort -> {
                        if (com.yaolonga.dc.core.model.Sort.ASC.equals(sort.getValue())) {
                            return Sort.Order.asc(sort.getField());
                        }
                        return Sort.Order.desc(sort.getField());
                    })
                    .collect(Collectors.toList());
            parse = parse.sort(Sort.by(orders));
        }

        if (!searchModel.getColumns().isEmpty()){
            parse = parse.columns(searchModel.getColumns());
        }
        return parse;
    }

    @Override
    public <T> Flux<T> executeAsync(List<GroupNode> astNode, Class<T> bean) {
        NodeParser<Query> queryNodeParser = nodeParser();
        Query query = queryNodeParser.parse(astNode, bean);
        return list(bean, query);
    }

    private <T> Flux<T> list(Class<T> bean, Query query) {
        return r2dbcEntityTemplate
                .select(query, bean)
                .onErrorResume(e -> {
                    log.error("R2dbcNodeExecutor 执行异常: {}", e.getMessage(), e);
                    return Mono.error(new NodeExecuteException("R2dbcNodeExecutor 执行异常: ", e));
                });
    }

    private <T> Mono<DCPage<T>> page(Query baseQuery, int page, int pageSize, Class<T> entityClass) {
        int skip = (page - 1) * pageSize;
        int limit = pageSize;
        // 采用id主键解决分页排序，出现重复记录问题
        // 获取实体类中带有@Id注解的字段名称
        Query pageQuery = baseQuery.offset(skip).limit(limit);
        Field idField = Arrays.stream(entityClass.getDeclaredFields()).filter(field -> field.isAnnotationPresent(Id.class)).findFirst().orElse(null);
        if (idField != null) {
            String dbFieldName = StringUtil.camelToUnderline(idField.getName());
            Column annotation = idField.getAnnotation(Column.class);
            if (annotation != null && StringUtils.hasText(annotation.value())) {
                dbFieldName = annotation.value();
            }
            pageQuery = pageQuery.sort(Sort.by(dbFieldName).ascending());
        }

        Mono<Long> countMono = r2dbcEntityTemplate.count(baseQuery, entityClass);
        Query finalPageQuery = pageQuery;
        return countMono
                .switchIfEmpty(Mono.just(0L))
                .flatMap(count -> {
                    if (count <= 0) {
                        DCPage<T> data = new DCPage();
                        data.setTotalPage(0);
                        data.setTotal(0);
                        data.setData(EMPTY_LIST);
                        data.setPage(page);
                        data.setTotalPage(0);
                        data.setPageSize(pageSize);
                        return Mono.just(data);
                    }
                    Flux<T> selectFlux = r2dbcEntityTemplate.select(finalPageQuery, entityClass);
                    return selectFlux
                            .buffer()
                            .single((List<T>) EMPTY_LIST)
                            .map(list -> {
                                DCPage<T> data = new DCPage();
                                data.setTotal(count);
                                data.setData(list);
                                data.setPage(page);
                                data.setPageSize(pageSize);
                                data.setTotalPage(count % pageSize == 0 ? count / pageSize : count / pageSize + 1);
                                return data;
                            });
                });
    }
}
